/*
 *	This file is part of Transdroid <http://www.transdroid.org>
 *
 *	Transdroid is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *
 *	Transdroid is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with Transdroid.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package org.transdroid.daemon.adapters.vuze;

import org.apache.openjpa.lib.util.Base16Encoder;
import org.transdroid.core.gui.log.Log;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonMethod;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.task.SetTransferRatesTask;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * An adapter that allows for easy access to Vuze torrent data. Communication
 * is handled via the XML-RPC protocol.
 *
 * @author erickok
 */
public class VuzeAdapter implements IDaemonAdapter {

    private static final String LOG_NAME = "Vuze daemon";

    private static final String RPC_URL = "/process.cgi";

    private VuzeXmlOverHttpClient rpcclient;
    private DaemonSettings settings;

    private Long savedConnectionID;
    private Long savedPluginID;
    private Long savedDownloadManagerID;
    private Long savedTorrentManagerID;
    private Long savedPluginConfigID;

    public VuzeAdapter(DaemonSettings settings) {
        this.settings = settings;
    }

    @Override
    public DaemonTaskResult executeTask(Log log, DaemonTask task) {

        try {
            switch (task.getMethod()) {
                case Retrieve:

                    Object result = makeVuzeCall(DaemonMethod.Retrieve, "getDownloads");
                    return new RetrieveTaskSuccessResult((RetrieveTask) task, onTorrentsRetrieved(log, result), null);

                case GetFileList:

                    // Retrieve a listing of the files in some torrent
                    Object fresult = makeVuzeCall(DaemonMethod.GetFileList, "getDiskManagerFileInfo", task.getTargetTorrent(), new Object[]{});
                    return new GetFileListTaskSuccessResult((GetFileListTask) task, onTorrentFilesRetrieved(fresult, task.getTargetTorrent()));

                case AddByFile:

                    byte[] bytes;
                    FileInputStream in = null;
                    try {
                        // Request to add a torrent by local .torrent file
                        String file = ((AddByFileTask) task).getFile();
                        in = new FileInputStream(new File(URI.create(file)));
                        bytes = new byte[in.available()];
                        //noinspection ResultOfMethodCallIgnored All bytes read at once, so no need to check result
                        in.read(bytes, 0, in.available());
                        in.close();
                    } catch (FileNotFoundException e) {
                        return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
                    } catch (IllegalArgumentException e) {
                        return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, "Invalid local URI"));
                    } catch (Exception e) {
                        return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
                    } finally {
                        try {
                            if (in != null)
                                in.close();
                        } catch (IOException e) {
                            // Ignore; it was already closed or never opened
                        }
                    }
                    makeVuzeCall(DaemonMethod.AddByFile, "createFromBEncodedData[byte[]]", new String[]{Base16Encoder.encode(bytes)});
                    return new DaemonTaskSuccessResult(task);

                case AddByUrl:

                    // Request to add a torrent by URL
                    String url = ((AddByUrlTask) task).getUrl();
                    makeVuzeCall(DaemonMethod.AddByUrl, "addDownload[URL]", new URL[]{new URL(url)});
                    return new DaemonTaskSuccessResult(task);

                case Remove:

                    // Remove a torrent
                    RemoveTask removeTask = (RemoveTask) task;
                    if (removeTask.includingData()) {
                        makeVuzeCall(DaemonMethod.Remove, "remove[boolean,boolean]", task.getTargetTorrent(), new String[]{"true", "true"});
                    } else {
                        makeVuzeCall(DaemonMethod.Remove, "remove", task.getTargetTorrent(), new Object[]{});
                    }
                    return new DaemonTaskSuccessResult(task);

                case Pause:

                    // Pause a torrent
                    makeVuzeCall(DaemonMethod.Pause, "stop", task.getTargetTorrent(), new Object[]{});
                    return new DaemonTaskSuccessResult(task);

                case PauseAll:

                    // Resume all torrents
                    makeVuzeCall(DaemonMethod.ResumeAll, "stopAllDownloads");
                    return new DaemonTaskSuccessResult(task);

                case Resume:

                    // Resume a torrent
                    makeVuzeCall(DaemonMethod.Start, "restart", task.getTargetTorrent(), new Object[]{});
                    return new DaemonTaskSuccessResult(task);

                case ResumeAll:

                    // Resume all torrents
                    makeVuzeCall(DaemonMethod.ResumeAll, "startAllDownloads");
                    return new DaemonTaskSuccessResult(task);

                case SetFilePriorities:

                    // For each of the chosen files belonging to some torrent, set the priority
                    SetFilePriorityTask prioTask = (SetFilePriorityTask) task;
                    // One at a time; Vuze doesn't seem to support setting the isPriority or isSkipped on (a subset of) all files at once
                    for (TorrentFile forFile : prioTask.getForFiles()) {
                        if (prioTask.getNewPriority() == Priority.Off) {
                            makeVuzeCall(DaemonMethod.SetFilePriorities, "setSkipped[boolean]", Long.parseLong(forFile.getKey()), new String[]{"true"});
                        } else if (prioTask.getNewPriority() == Priority.High) {
                            makeVuzeCall(DaemonMethod.SetFilePriorities, "setSkipped[boolean]", Long.parseLong(forFile.getKey()), new String[]{"false"});
                            makeVuzeCall(DaemonMethod.SetFilePriorities, "setPriority[boolean]", Long.parseLong(forFile.getKey()), new String[]{"true"});
                        } else {
                            makeVuzeCall(DaemonMethod.SetFilePriorities, "setSkipped[boolean]", Long.parseLong(forFile.getKey()), new String[]{"false"});
                            makeVuzeCall(DaemonMethod.SetFilePriorities, "setPriority[boolean]", Long.parseLong(forFile.getKey()), new String[]{"false"});
                        }
                    }
                    return new DaemonTaskSuccessResult(task);

                case SetTransferRates:

                    // Request to set the maximum transfer rates
                    SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
                    makeVuzeCall(DaemonMethod.SetTransferRates, "setBooleanParameter[String,boolean]", new Object[]{"Auto Upload Speed Enabled", false});
                    makeVuzeCall(DaemonMethod.SetTransferRates, "setCoreIntParameter[String,int]", new Object[]{"Max Upload Speed KBs", (ratesTask.getUploadRate() == null ? 0 :
                            ratesTask.getUploadRate())});
                    makeVuzeCall(DaemonMethod.SetTransferRates, "setCoreIntParameter[String,int]", new Object[]{"Max Download Speed KBs", (ratesTask.getDownloadRate() == null ? 0 :
                            ratesTask.getDownloadRate())});
                    return new DaemonTaskSuccessResult(task);

                default:
                    return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported, task.getMethod() + " is not supported by " + getType()));
            }
        } catch (DaemonException e) {
            return new DaemonTaskFailureResult(task, e);
        } catch (IOException e) {
            return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ConnectionError, e.toString()));
        }
    }

    private Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod, Torrent actOnTorrent, Object[] params) throws DaemonException {
        return makeVuzeCall(method, serverMethod, Long.parseLong(actOnTorrent.getUniqueID()), params, actOnTorrent.getStatusCode());
    }

    private Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod, Long actOnObject, Object[] params) throws DaemonException {
        return makeVuzeCall(method, serverMethod, actOnObject, params, null);
    }

    private Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod, Object[] params) throws DaemonException {
        return makeVuzeCall(method, serverMethod, null, params, null);
    }

    private Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod) throws DaemonException {
        return makeVuzeCall(method, serverMethod, null, new Object[]{}, null);
    }

    private synchronized Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod, Long actOnObject, Object[] params, TorrentStatus torrentStatus) throws DaemonException {

        // TODO: It would be nicer to now split each of these steps into separate makeVuzeCalls when there are multiple logical steps such as stopping a torrent before removing it

        // Initialise the HTTP client
        if (rpcclient == null) {
            initialise();
        }
        if (settings.getAddress() == null || settings.getAddress().equals("")) {
            throw new DaemonException(DaemonException.ExceptionType.AuthenticationFailure, "No host name specified.");
        }

        if (savedConnectionID == null || savedPluginID == null) {
            // Get plug-in interface (for connection and plug-in object IDs)
            Map<String, Object> plugin = rpcclient.callXMLRPC(null, "getSingleton", null, null, false);
            if (!plugin.containsKey("_connection_id")) {
                throw new DaemonException(ExceptionType.UnexpectedResponse, "No connection ID returned on getSingleton request.");
            }
            savedConnectionID = (Long) plugin.get("_connection_id");
            savedPluginID = (Long) plugin.get("_object_id");
        }

        // If no specific torrent was provided, get the download manager or plugin config to execute the method against
        long vuzeObjectID;
        if (actOnObject == null) {
            if (method == DaemonMethod.SetTransferRates) {

                // Execute this method against the plugin config (setParameter)
                if (savedPluginConfigID == null) {
                    // Plugin config needed, but we don't know it's ID yet
                    Map<String, Object> config = rpcclient.callXMLRPC(savedPluginID, "getPluginconfig", null, savedConnectionID, false);
                    if (!config.containsKey("_object_id")) {
                        throw new DaemonException(ExceptionType.UnexpectedResponse, "No plugin config ID returned on getPluginconfig");
                    }
                    savedPluginConfigID = (Long) config.get("_object_id");
                }
                // We stored the plugin config ID, so no need to ask for it again
                vuzeObjectID = savedPluginConfigID;

            } else if (serverMethod.equals("createFromBEncodedData[byte[]]")) {

                // Execute this method against the torrent manager (createFromBEncodedData)
                if (savedTorrentManagerID == null) {
                    // Download manager needed, but we don't know it's ID yet
                    Map<String, Object> manager = rpcclient.callXMLRPC(savedPluginID, "getTorrentManager", null, savedConnectionID, false);
                    if (!manager.containsKey("_object_id")) {
                        throw new DaemonException(ExceptionType.UnexpectedResponse, "No torrent manager ID returned on getTorrentManager");
                    }
                    savedTorrentManagerID = (Long) manager.get("_object_id");
                }
                // We stored the torrent manager ID, so no need to ask for it again
                vuzeObjectID = savedTorrentManagerID;

                // And we will need the download manager as well later on (for addDownload after createFromBEncodedData)
                if (savedDownloadManagerID == null) {
                    // Download manager needed, but we don't know it's ID yet
                    Map<String, Object> manager = rpcclient.callXMLRPC(savedPluginID, "getDownloadManager", null, savedConnectionID, false);
                    if (!manager.containsKey("_object_id")) {
                        throw new DaemonException(ExceptionType.UnexpectedResponse, "No download manager ID returned on getDownloadManager");
                    }
                    savedDownloadManagerID = (Long) manager.get("_object_id");
                }

            } else {

                // Execute this method against download manager (addDownload, startAllDownloads, etc.)
                if (savedDownloadManagerID == null) {
                    // Download manager needed, but we don't know it's ID yet
                    Map<String, Object> manager = rpcclient.callXMLRPC(savedPluginID, "getDownloadManager", null, savedConnectionID, false);
                    if (!manager.containsKey("_object_id")) {
                        throw new DaemonException(ExceptionType.UnexpectedResponse, "No download manager ID returned on getDownloadManager");
                    }
                    savedDownloadManagerID = (Long) manager.get("_object_id");
                }
                // We stored the download manager ID, so no need to ask for it again
                vuzeObjectID = savedDownloadManagerID;

            }
        } else {
            vuzeObjectID = actOnObject;
        }

        if (method == DaemonMethod.Remove && torrentStatus != null && torrentStatus != TorrentStatus.Paused) {
            // Vuze, for some strange reason, wants us to stop the torrent first before removing it
            rpcclient.callXMLRPC(vuzeObjectID, "stop", new Object[]{}, savedConnectionID, false);
        }

        boolean paramsAreVuzeObjects = false;
        if (serverMethod.equals("createFromBEncodedData[byte[]]")) {
            // Vuze does not directly add the torrent that we are uploading the file contents of
            // We first do the createFromBEncodedData call and next actually add it
            Map<String, Object> torrentData = rpcclient.callXMLRPC(vuzeObjectID, serverMethod, params, savedConnectionID, false);
            serverMethod = "addDownload[Torrent]";
            vuzeObjectID = savedDownloadManagerID;
            params = new String[]{torrentData.get("_object_id").toString()};
            paramsAreVuzeObjects = true;
        }

        // Call the actual method we wanted
        return rpcclient.callXMLRPC(vuzeObjectID, serverMethod, params, savedConnectionID, paramsAreVuzeObjects);

    }

    /**
     * Instantiates a Vuze XML over HTTP client with proper credentials.
     *
     * @throws DaemonException On conflicting settings (i.e. user authentication but no password or username provided)
     */
    private void initialise() throws DaemonException {

        this.rpcclient = new VuzeXmlOverHttpClient(settings, buildWebUIUrl());

    }

    /**
     * Build the URL of the Vuze XML over HTTP plugin listener from the user settings.
     *
     * @return The URL of the RPC API
     */
    private String buildWebUIUrl() {
        return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + RPC_URL;
    }

    @SuppressWarnings("unchecked")
    private List<Torrent> onTorrentsRetrieved(Log log, Object result) throws DaemonException {

        Map<String, Object> response = (Map<String, Object>) result;

        // We might have an empty list if no torrents are on the server
        if (response == null) {
            return new ArrayList<>();
        }

        log.d(LOG_NAME, response.toString().length() > 300 ? response.toString().substring(0, 300) + "... (" + response.toString().length() + " chars)" : response.toString());

        List<Torrent> torrents = new ArrayList<>();

        // Parse torrent list from Vuze response, which is a map list of ENTRYs
        for (String key : response.keySet()) {

            /**
             *  Every Vuze ENTRY is a map of key-value pairs with information, or a key-map pair with that map being a mapping of key-value pairs with information
             *  VuzeXmlTorrentListResponse.txt in the Transdroid wiki shows a full example response, but it looks something like:
             *  ENTRY0={
             position=1,
             torrent_file=/home/erickok/.azureus/torrents/ubuntu.torrent,
             name=ubuntu-9.04-desktop-i386.iso,
             torrent={
             size=732909568,
             creation_date=1240473087
             }
             }
             */
            Map<String, Object> info = (Map<String, Object>) response.get(key);
            if (info == null || !info.containsKey("_object_id") || info.get("_object_id") == null) {
                // No valid XML data object returned
                throw new DaemonException(DaemonException.ExceptionType.UnexpectedResponse, "Map of objects returned by Vuze, but these object do not have some <info> attached or no <_object_id> is available");
            }
            Map<String, Object> torrentinfo = (Map<String, Object>) info.get("torrent");
            Map<String, Object> statsinfo = (Map<String, Object>) info.get("stats");
            Map<String, Object> scrapeinfo = (Map<String, Object>) info.get("scrape_result");
            Map<String, Object> announceinfo = (Map<String, Object>) info.get("announce_result");
            int scrapeSeedCount = ((Long) scrapeinfo.get("seed_count")).intValue();
            int scrapeNonSeedCount = ((Long) scrapeinfo.get("non_seed_count")).intValue();
            String error = (String) info.get("error_state_details");
            error = error != null && error.equals("") ? null : error;
            int announceSeedCount = ((Long) announceinfo.get("seed_count")).intValue();
            int announceNonSeedCount = ((Long) announceinfo.get("non_seed_count")).intValue();
            int rateDownload = ((Long) statsinfo.get("download_average")).intValue();
            Double availability = (Double) statsinfo.get("availability");
            Long size = torrentinfo != null ? (Long) torrentinfo.get("size") : 0;

            torrents.add(new Torrent(
                    (Long) info.get("_object_id"), // id
                    info.get("_object_id").toString(), // hash	//(String) torrentinfo.get("hash"), // hash
                    info.get("name").toString().trim(), // name
                    convertTorrentStatus((Long) info.get("state")), // status
                    statsinfo.get("target_file_or_dir") + "/", // locationDir
                    rateDownload, // rateDownload
                    ((Long) statsinfo.get("upload_average")).intValue(), // rateUpload
                    announceSeedCount, // seedersConnected
                    scrapeSeedCount, // seedersKnown
                    announceNonSeedCount, // leechersConnected
                    scrapeNonSeedCount, // leechersKnown
                    (rateDownload > 0 ? (int) ((Long) statsinfo.get("remaining") / rateDownload) : -1), // eta (bytes left / rate download, if rate > 0)
                    (Long) statsinfo.get("downloaded"), // downloadedEver
                    (Long) statsinfo.get("uploaded"), // uploadedEver
                    size, // totalSize
                    (float) ((Long) statsinfo.get("downloaded")) / (float) (size), // partDone (downloadedEver / totalSize)
                    Math.min(availability.floatValue(), 1f),
                    null, // TODO: Implement Vuze label support
                    new Date((Long) statsinfo.get("time_started")), // dateAdded
                    null, // Unsupported?
                    error,
                    settings.getType()));

        }

        return torrents;

    }

    @SuppressWarnings("unchecked")
    private List<TorrentFile> onTorrentFilesRetrieved(Object result, Torrent torrent) {

        Map<String, Object> response = (Map<String, Object>) result;

        // We might have an empty list
        if (response == null) {
            return new ArrayList<>();
        }

        //DLog.d(LOG_NAME, response.toString().length() > 300? response.toString().substring(0, 300) + "... (" + response.toString().length() + " chars)": response.toString());

        List<TorrentFile> files = new ArrayList<>();

        // Parse torrent file list from Vuze response, which is a map list of ENTRYs
        for (String key : response.keySet()) {

            /**
             *  Every Vuze ENTRY is a map of key-value pairs with information
             *  For file lists, it looks something like:
             *  ENTRY2={
             is_deleted=false,
             length=298,
             downloaded=298,
             is_priority=false,
             first_piece_number=726,
             is_skipped=false,
             file=/var/data/Downloads/Some.Torrent/OneFile.txt,
             _object_id=443243294889782236,
             num_pieces=1,
             access_mode=1
             }
             */
            Map<String, Object> info = (Map<String, Object>) response.get(key);
            String file = (String) info.get("file");

            files.add(new TorrentFile(
                    String.valueOf(info.get("_object_id")),
                    new File(file).getName(), // name
                    (file.length() > torrent.getLocationDir().length() ? file.substring(torrent.getLocationDir().length()) : file), // name
                    file, // fullPath
                    (Long) info.get("length"), // size
                    (Long) info.get("downloaded"), // downloaded
                    convertVuzePriority((String) info.get("is_skipped"), (String) info.get("is_priority")))); // priority

        }

        return files;

    }

    private Priority convertVuzePriority(String isSkipped, String isPriority) {
        return isSkipped.equals("true") ? Priority.Off : (isPriority.equals("true") ? Priority.High : Priority.Normal);
    }

    private TorrentStatus convertTorrentStatus(Long state) {

        switch (state.intValue()) {
            case 2:
                return TorrentStatus.Checking;
            case 4:
                return TorrentStatus.Downloading;
            case 5:
                return TorrentStatus.Seeding;
            case 7:
                return TorrentStatus.Paused;
            case 8:
                return TorrentStatus.Error;
        }
        return TorrentStatus.Unknown;
    }

    @Override
    public Daemon getType() {
        return settings.getType();
    }

    @Override
    public DaemonSettings getSettings() {
        return this.settings;
    }

}
